#include "WavefrontOBJ.hpp"

Material::Material() : textured(false){}

uint WavefrontOBJ::programID = 0;

uint WavefrontOBJ::MatrixID = 0;
uint WavefrontOBJ::ViewMatrixID = 0;
uint WavefrontOBJ::ModelMatrixID = 0;

uint WavefrontOBJ::LightID = 0;
uint WavefrontOBJ::secLightID = 0;
uint WavefrontOBJ::TextureID = 0;

uint WavefrontOBJ::ambientID = 0;
uint WavefrontOBJ::diffuseID = 0;
uint WavefrontOBJ::specularID = 0;

uint WavefrontOBJ::flagID = 0;

bool WavefrontOBJ::loadWavefrontOBJ(const char* _path){
    cout << "Loading: " << _path << endl;

    Importer importer;

    const aiScene* scene = importer.ReadFile(_path, aiProcess_FlipUVs );
	if( !scene) {
		fprintf( stderr, importer.GetErrorString());
		return false;
	}

	//const aiTexture* mTexture = scene->mTextures[0];
	numMaterial = scene->mNumMaterials;
	material     = new Material[numMaterial];

    for (uint m = 0; m < numMaterial; ++m)
    {
        aiMaterial *material = scene->mMaterials[m];
        if(!initMaterial(material, m)) return false;
    }

	const aiMesh* mesh;
    numMeshes = scene->mNumMeshes;

    vertexBuffer =  (uint*)malloc(sizeof(uint) * numMeshes);
    UVBuffer     =  (uint*)malloc(sizeof(uint) * numMeshes);
    normalBuffer =  (uint*)malloc(sizeof(uint) * numMeshes);

    bufferSize   =  (uint*)malloc(sizeof(uint) * numMeshes);

    indicies     =  (uint*)malloc(sizeof(uint) * numMeshes);

    indiciesSize =  (uint*)malloc(sizeof(uint) * numMeshes);

    textureIndex =  (uint*)malloc(sizeof(uint) * numMeshes);

    hasUV     = (bool*)malloc(sizeof(bool) * numMeshes);
    hasNormal = (bool*)malloc(sizeof(bool) * numMeshes);
    hasFaces  = (bool*)malloc(sizeof(bool) * numMeshes);

    for(uint i = 0; i < numMeshes; i++){
        mesh = scene->mMeshes[i];

    	if(!initVertexBuffer(mesh->mVertices, mesh->mNumVertices, i)){
            return false;
        }

    	if(!(mesh->HasTextureCoords(0) && initUVBuffer(mesh->mTextureCoords[0], mesh->mNumVertices, i))){
            hasUV[i] = false;
    	} else {
            hasUV[i] = true;
    	}

        if(!(mesh->HasNormals() && initNormalBuffer(mesh->mNormals, mesh->mNumVertices, i))){
            hasNormal[i] = false;
        } else {
            hasNormal[i] = true;
    	}

        if(!(mesh->HasFaces() && initFaceBuffer(mesh->mFaces, mesh->mNumFaces, i))){
            hasFaces[i] = false;
        } else {
            hasFaces[i] = true;
    	}

        if(!initTextureIndex(mesh->mMaterialIndex, i)){
            return false;
        }
    }

    return true;
}

bool WavefrontOBJ::initVertexBuffer(aiVector3D *mVertices, uint mNumVertices, uint vbn){
	bufferSize[vbn] = mNumVertices;

	glGenBuffers(1, &vertexBuffer[vbn]);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer[vbn]);
	glBufferData(GL_ARRAY_BUFFER, mNumVertices * sizeof(aiVector3D), &mVertices[0], GL_STATIC_DRAW);

	if(vertexBuffer[vbn] == 0){
        return false;
	}

    return true;
}

bool WavefrontOBJ::initUVBuffer(aiVector3D *mUv, uint mNumUv, uint uvn){
    std::vector<glm::vec2> uvs;
    for(unsigned int i = 0; i < mNumUv; i++){
		aiVector3D UVW = mUv[i];
		uvs.push_back(glm::vec2(UVW.x, UVW.y));
	}

	glGenBuffers(1, &UVBuffer[uvn]);
	glBindBuffer(GL_ARRAY_BUFFER, UVBuffer[uvn]);
	glBufferData(GL_ARRAY_BUFFER, mNumUv * sizeof(vec2), &uvs[0], GL_STATIC_DRAW);

	if(UVBuffer[uvn] == 0){
        return false;
	}

    return true;
}

bool WavefrontOBJ::initNormalBuffer(aiVector3D *mNormal, uint mNumNormal, uint non){
	glGenBuffers(1, &normalBuffer[non]);
	glBindBuffer(GL_ARRAY_BUFFER, normalBuffer[non]);
	glBufferData(GL_ARRAY_BUFFER, mNumNormal * sizeof(aiVector3D), &mNormal[0], GL_STATIC_DRAW);

	if(normalBuffer[non] == 0){
        return false;
	}

    return true;
}

bool WavefrontOBJ::initFaceBuffer(aiFace *mFaces, uint mNumFaces, uint fan){
    const uint ni = mFaces->mNumIndices;

    indiciesSize[fan] = mNumFaces * ni;

    uint _indicies[mNumFaces * ni];

    for(uint i = 0; i < mNumFaces; i++){
        for(uint j = 0; j < ni; j++){
            _indicies[i*ni + j] = mFaces[i].mIndices[j];
        }
    }

    glGenBuffers(1, &indicies[fan]);
	glBindBuffer(GL_ARRAY_BUFFER, indicies[fan]);
	glBufferData(GL_ARRAY_BUFFER, mNumFaces * ni * sizeof(uint), &_indicies[0], GL_STATIC_DRAW);

	if(indicies[fan] == 0){
        return false;
	}

    return true;
}

bool WavefrontOBJ::loadTexture(string path, uint mn){
    Image img;
    img.read(path);

    cout << path << endl;

    int w = img.columns();
    int h = img.rows();

    cout << w << " " << h << endl;

    Blob blob;
    img.magick( "RGB" );
    img.write(&blob);

    glGenTextures(1, &material[mn].texture );

	glBindTexture ( GL_TEXTURE_2D, material[mn].texture);
	glTexImage2D  ( GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB,GL_UNSIGNED_BYTE,(GLvoid*)blob.data() );

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );

    glGenerateMipmap( GL_TEXTURE_2D );

	if(material[mn].texture == 0){
        return false;
	}

    return true;
}

bool WavefrontOBJ::initMaterial(aiMaterial *mMaterial, uint mn){
    aiString aiImage;  // filename
    string   sPath(path);
    string   images("images/");

    if(mMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &aiImage) == AI_SUCCESS) {
        string sImage(aiImage.data);

        if(loadTexture(sPath + images + sImage, mn)){
            material[mn].textured = true;
        } else {
            return false;
        }
    }

    for(uint i = 0; i < mMaterial->mNumProperties; i++){
        if(mMaterial->mProperties[i]->mKey == aiString("$clr.ambient")){
            if(0x1 == mMaterial->mProperties[i]->mType){
                float *data = (float *)mMaterial->mProperties[i]->mData;
                material[mn].ambient.x = data[0];
                material[mn].ambient.y = data[1];
                material[mn].ambient.z = data[2];
            }
        } else if(mMaterial->mProperties[i]->mKey == aiString("$clr.diffuse")){
            if(0x1 == mMaterial->mProperties[i]->mType){
                float *data = (float *)mMaterial->mProperties[i]->mData;
                material[mn].diffuse.x = data[0];
                material[mn].diffuse.y = data[1];
                material[mn].diffuse.z = data[2];
            }
        } else if(mMaterial->mProperties[i]->mKey == aiString("$clr.specular")){
            if(0x1 == mMaterial->mProperties[i]->mType){
                float *data = (float *)mMaterial->mProperties[i]->mData;
                material[mn].specular.x = data[0];
                material[mn].specular.y = data[1];
                material[mn].specular.z = data[2];
            }
        }
        //cout << mMaterial->mProperties[i]->mKey.C_Str() << endl;
    }

    return true;
}

bool WavefrontOBJ::initTextureIndex(uint mMaterialIndex, int ms){
    textureIndex[ms] = mMaterialIndex;

    return true;
}

bool WavefrontOBJ::loadProgram(){
    cout << "Load program" << endl;

    if(programID == 0){
       programID = LoadShaders( "StandardShading.vert", "StandardShading.frag" );
       glUseProgram(programID);
    }

    MatrixID        = glGetUniformLocation(programID, "MVP");
    ViewMatrixID    = glGetUniformLocation(programID, "V");
    ModelMatrixID   = glGetUniformLocation(programID, "M");
    LightID         = glGetUniformLocation(programID, "LightPosition_worldspace");
    secLightID      = glGetUniformLocation(programID, "secLightPosition_worldspace");
    TextureID       = glGetUniformLocation(programID, "myTextureSampler");
    ambientID       = glGetUniformLocation(programID, "ambient");
    diffuseID       = glGetUniformLocation(programID, "diffuse");
    specularID      = glGetUniformLocation(programID, "specular");
    flagID          = glGetUniformLocation(programID, "flag");

    return true;
}

WavefrontOBJ::WavefrontOBJ(string _path, string _name) : path(_path), name(_name) {
    ModelMatrix = mat4(1.0);

    loaded = true;

    light = false;
    cameraLight = false;
    noise = false;
    texture = false;

    if(!loadWavefrontOBJ((path + name).c_str())){
        cout << "Can't load model: " << _path << endl;
        loaded = false;
    }

    loadProgram();
}

int WavefrontOBJ::createFlag(Material *material){
    int re = 0;

    if(light) re |= 0x1;
    if(cameraLight) re |= 0x10;
    if(noise && material->textured) re |= 0x1000;

    if(material->textured && texture) re |= 0x100;

    //cout << re << endl;

    return re;
}

void WavefrontOBJ::lightChange(){
    light = !light;
}

void WavefrontOBJ::cameraLightChange(){
    cameraLight = !cameraLight;
}

void WavefrontOBJ::noiseChange(){
    noise = !noise;
}

void WavefrontOBJ::textureChange(){
    texture = !texture;
}


bool WavefrontOBJ::draw(mat4 _VP, mat4 _V, vec3 camerePosition){
    if(loaded){
        glUseProgram(programID);

        mat4 MVP = _VP * ModelMatrix;

		glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
		glUniformMatrix4fv(ModelMatrixID, 1, GL_FALSE, &ModelMatrix[0][0]);
		glUniformMatrix4fv(ViewMatrixID, 1, GL_FALSE, &_V[0][0]);

		glUniform3f(LightID, 0, 5, 0);
        glUniform3f(secLightID, camerePosition.x, camerePosition.y, camerePosition.z);

        for(uint i = 0; i < numMeshes; i++){
            Material *pMaterial = &material[textureIndex[i]];

            glUniform3f(ambientID, pMaterial->ambient.x, pMaterial->ambient.y, pMaterial->ambient.z);
            glUniform3f(diffuseID, pMaterial->diffuse.x, pMaterial->diffuse.y, pMaterial->diffuse.z);
            glUniform3f(specularID, pMaterial->specular.x, pMaterial->specular.y, pMaterial->specular.z);

            if(pMaterial->textured){
                glUniform1i(TextureID, 0);
                glActiveTexture(GL_TEXTURE0);
                glBindTexture(GL_TEXTURE_2D, pMaterial->texture);
            }

            glUniform1i(flagID, createFlag(pMaterial));

            glEnableVertexAttribArray(0);
                glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer[i]);
                glVertexAttribPointer(
                    0,                  // attribute
                    3,                  // size
                    GL_FLOAT,           // type
                    GL_FALSE,           // normalized?
                    0,                  // stride
                    (void*)0            // array buffer offset
            );

            if(hasUV[i]){
                // 2nd attribute buffer : UVs
                glEnableVertexAttribArray(1);
                glBindBuffer(GL_ARRAY_BUFFER, UVBuffer[i]);
                glVertexAttribPointer(
                    1,                                // attribute
                    2,                                // size
                    GL_FLOAT,                         // type
                    GL_FALSE,                         // normalized?
                    0,                                // stride
                    (void*)0                          // array buffer offset
                );
            }

            if(hasNormal[i]){
                // 3rd attribute buffer : normals
                glEnableVertexAttribArray(2);
                glBindBuffer(GL_ARRAY_BUFFER, normalBuffer[i]);
                glVertexAttribPointer(
                    2,                                // attribute
                    3,                                // size
                    GL_FLOAT,                         // type
                    GL_FALSE,                         // normalized?
                    0,                                // stride
                    (void*)0                          // array buffer offset
                );
            }

            if(hasFaces[i]){
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indicies[i]);

                glDrawElements(
                    GL_TRIANGLES,      // mode
                    indiciesSize[i],    // count
                    GL_UNSIGNED_INT,   // type
                    (void*)0           // element array buffer offset
                );
            } else {
                glDrawArrays(GL_TRIANGLES, 0, bufferSize[i]);
            }

            glDisableVertexAttribArray(0);
            glDisableVertexAttribArray(1);
            glDisableVertexAttribArray(2);
        }
    }

    return true;
}
